Assessing the Commitment of Traders Report

Tim Udelsman | 5/8/2017 | Prof. Csaba

Introduction

I intend to examine the relationship between the CFTC's Commitment of Traders reports and future commodity price movements. If any predictive power is determined, I will then construct a simulated portfolio that trades over time with the market and compare that to historical market performance. There are a few caveats to take into account with this...

  • To determine predictive powers I will be using correlations, but it's important to remember correlations do not mean causation
  • The simulated portfolio I use will not be equivalent to the real thing - transaction costs, bid/ask spread costs, taxes, etc. are all additonal expenses to consider
  • Finally, backtesting is a hallmark of quantitative trading but it's important to remember correlations change over time and past results do not predict future results

The Commodities and Futures Trading Commission (CFTC) publishes its Commitment of Traders (COT) report weekly

The report highlights the number of traders with long or short positions in futures or option in...

  • Energy (Crude, Natural Gal, RBOB))
  • Metals (Gold, Silver, Copper)
  • Agriculture (Corn, Soybeans, Wheat)
  • Foreign Exchange Rates
  • Much, much more

Further, the COT report also breaks down traders into...

  • Commercial Producers - those that are using futures and options to hedge their revenues
  • Institutional Money Managers - large fund managers (hedge, pension, mutual) typically making speculative bets
  • Swap Dealers - not very important for this project, mostly financial market makers
  • Public Traders - individual traders with positions large enough to require reporting to the CFTC (aka 'Dumb Money')

In [26]:
# Import necessary packages

# Import Quandl and FRED APIs with designated keys
import quandl
quandl.ApiConfig.api_key = '7D_fgezPtnQmwSEpAp9e'

from fredapi import Fred
fred = Fred(api_key='413d49541fa9fdb3f97029685315d5a5')

import pandas as pd
import numpy as np

# Import plotting packages. Despite its relative difficulty I will use plotly for its design and interactive features
from plotly.offline import iplot, iplot_mpl  # plotting functions
import plotly.graph_objs as go               # ditto
import cufflinks as cf                       # gives us df.iplot that feels like df.plot
import plotly.plotly as py
cf.set_config_file(offline=True, offline_show_link=False)


To import and help with data management - we will use the Quandl financial data API

Quandl is a financial data provider for a number of quantitative financial firms -

  • While their paid data is very comprehensive, they also collect free financial data
  • This free data is collected from across the internet and provide easy acess via their API tool

Let's begin to look at the data - use the Quandl search function to find necessary 'data codes'


In [27]:
# Import the COT report on 'Light Sweet WTI Crude' and examine
WTI_COT = quandl.get('CFTC/CL_FO_ALL')
WTI_COT.head()


Out[27]:
Open Interest Producer/Merchant/Processor/User Longs Producer/Merchant/Processor/User Shorts Swap Dealer Longs Swap Dealer Shorts Swap Dealer Spreads Money Manager Longs Money Manager Shorts Money Manager Spreads Other Reportable Longs Other Reportable Shorts Other Reportable Spreads Total Reportable Longs Total Reportable Shorts Non Reportable Longs Non Reportable Shorts
Date
2006-06-13 1697172.0 363858.0 455968.0 145598.0 93366.0 414973.0 121365.0 91007.0 195750.0 53849.0 31427.0 340534.0 1635927.0 1623025.0 61245.0 74147.0
2006-06-20 1560200.0 325138.0 425078.0 154153.0 84988.0 386409.0 116693.0 88336.0 176989.0 48503.0 32191.0 295329.0 1503214.0 1489321.0 56986.0 70880.0
2006-06-27 1591677.0 325150.0 423772.0 147432.0 102669.0 387407.0 128005.0 69731.0 182294.0 50065.0 41791.0 308223.0 1528576.0 1515886.0 63100.0 75790.0
2006-07-03 1635218.0 327513.0 438406.0 144389.0 108192.0 392482.0 142472.0 59261.0 189521.0 48833.0 44659.0 320410.0 1565620.0 1552931.0 69599.0 82287.0
2006-07-11 1700141.0 348625.0 452829.0 138045.0 117903.0 397823.0 150447.0 62864.0 201528.0 55438.0 48701.0 334741.0 1626647.0 1616390.0 73494.0 83751.0

In [28]:
# Also import the COT reports on 'Gold Comex - Futures and Options' for another dataset to look at later
Gold_COT = quandl.get('CFTC/GC_FO_ALL')
Gold_COT.head()


Out[28]:
Open Interest Producer/Merchant/Processor/User Longs Producer/Merchant/Processor/User Shorts Swap Dealer Longs Swap Dealer Shorts Swap Dealer Spreads Money Manager Longs Money Manager Shorts Money Manager Spreads Other Reportable Longs Other Reportable Shorts Other Reportable Spreads Total Reportable Longs Total Reportable Shorts Non Reportable Longs Non Reportable Shorts
Date
2006-06-13 382474.0 42587.0 128892.0 19370.0 66611.0 22998.0 103211.0 30916.0 46704.0 33230.0 10656.0 58412.0 326513.0 365190.0 55961.0 17284.0
2006-06-20 390281.0 47440.0 126992.0 22404.0 64682.0 25806.0 94632.0 30963.0 48730.0 32947.0 11142.0 64458.0 336417.0 372774.0 53864.0 17507.0
2006-06-27 384321.0 48449.0 121890.0 22425.0 62640.0 25151.0 91042.0 32287.0 49614.0 31307.0 11386.0 59762.0 327749.0 362729.0 56572.0 21592.0
2006-07-03 392868.0 48087.0 122568.0 22720.0 69714.0 26352.0 92545.0 30452.0 50821.0 32874.0 8677.0 63514.0 336914.0 372098.0 55955.0 20771.0
2006-07-11 427008.0 54509.0 130618.0 21840.0 79217.0 28805.0 104213.0 27624.0 54922.0 33165.0 11194.0 70944.0 368398.0 403323.0 58611.0 23685.0

In [29]:
# Clear up some unneeded columns
WTI_COT = WTI_COT.drop(['Swap Dealer Spreads', 
              'Money Manager Spreads', 
              'Other Reportable Spreads', 
              'Non Reportable Longs', 
              'Non Reportable Shorts'], 
              axis = 1)

In [30]:
Gold_COT = Gold_COT.drop(['Swap Dealer Spreads', 
              'Money Manager Spreads', 
              'Other Reportable Spreads', 
              'Non Reportable Longs', 
              'Non Reportable Shorts'], 
              axis = 1)

In [31]:
# Rename merged dataframe column titles for clarity - these are closer names to what these headings actually mean
col_list = list(WTI_COT.columns) # Collect column titles in a list for renaming
# This is easy as both dataframes have the same headings
col_list[1] = 'Commercial Longs'
col_list[2] = 'Commercial Shorts'
col_list[3] = 'Speculator Longs'
col_list[4] = 'Speculator Shorts'
col_list[7] = 'Public Longs'
col_list[8] = 'Public Shorts'
WTI_COT.columns = [col_list]
Gold_COT.columns = [col_list]

We want to find historical prices for WTI and Gold to see if the COT reports have any predictive powers


In [32]:
# We will use the FRED API
# Customize FRED reports using API to read weekly, ending Tuesday (This saves time later on)
WTI_Price = fred.get_series('DCOILWTICO', 
                            observation_start = '2006-6-13',
                            observation_end = '2017-3-28',
                            frequency = 'wetu', # Weekly, ending Tuesday like COT reports
                            aggregation_method = 'eop')
WTI_Price = pd.DataFrame(WTI_Price)
WTI_Price = WTI_Price.iloc[::-1] # Need to save in descending order

Gold_Price = fred.get_series('GOLDAMGBD228NLBM', 
                             observation_start = '2006-6-13', 
                             observation_end = '2017-3-28',
                             frequency = 'wetu',
                             aggregation_method = 'eop')
Gold_Price = pd.DataFrame(Gold_Price)
Gold_Price = Gold_Price.iloc[::-1]

In [33]:
# Rename WTI_Price for clarity
WTI_Price.columns = ['WTI $/bbl']
WTI_Price.index.name = 'Date'
# Rename Gold_Price for clarity
Gold_Price.columns = ['Gold $/troy oz']
Gold_Price.index.name = 'Date'

From this data, we can draw conclusions as to whether the COT reports have any predictive information about where future WTI or Gold prices are headed

-- Data Manipulation --


In [34]:
# Check the shapes of each data set to ensure their lengths match for merging and correlation testing
Gold_Price.shape


Out[34]:
(564, 1)

In [35]:
WTI_Price.shape


Out[35]:
(564, 1)

In [36]:
# Merge the WTI and Gold price dataframes with their respective COT reports
# Be mindful of the date index as the COT report is ascending and price report descending in date
WTI_Merge = pd.merge(WTI_COT, WTI_Price, 
                     how = 'inner', # Use the 'inner' setting because we only want info where the two datasets intersect 
                     left_index = True,
                     right_index = True)
Gold_Merge = pd.merge(Gold_COT, Gold_Price,
                      how = 'inner',
                      left_index = True,
                      right_index = True)

-- Initial Looks at Data --


In [37]:
# Plot historical prices of WTI and gold overlayed with open interest to look for pattern
WTI_Trace = go.Scatter( 
            x = WTI_Merge.index,
            y = WTI_Merge['WTI $/bbl'],
            name = 'WTI $/bbl')
WTI_Trace2 = go.Scatter(
            x = WTI_Merge.index,
            y = WTI_Merge['Open Interest'],
            name = 'WTI Open Interest',
            yaxis = 'y2')

data1 = [WTI_Trace, WTI_Trace2]

Gold_Trace = go.Scatter(
            x = Gold_Merge.index,
            y = Gold_Merge['Gold $/troy oz'],
            name = 'Gold $/troy oz')
Gold_Trace2 = go.Scatter(
            x = Gold_Merge.index,
            y = Gold_Merge['Open Interest'],
            name = 'Gold Open Interest',
            yaxis = 'y2')

data2 = [Gold_Trace, Gold_Trace2]

layout = dict(
    title = 'Price versus Open Interest',
    width = 800,
    height = 400,
    yaxis = {'title':'Price ($)'},
    yaxis2 = dict(
        title='Open Interest',
        overlaying='y',
        side='right')
    )


fig = go.Figure(data=data1, layout=layout)
iplot(fig)

fig = go.Figure(data=data2, layout=layout)
iplot(fig)


-- Data Analysis --

Before going further, let's look for correlating variables


In [38]:
WTI_Merge.corrwith(WTI_Merge['WTI $/bbl'])


Out[38]:
Open Interest              0.018922
Commercial Longs           0.143875
Commercial Shorts         -0.295506
Speculator Longs          -0.195237
Speculator Shorts          0.321055
Money Manager Longs        0.009722
Money Manager Shorts      -0.603427
Public Longs              -0.399898
Public Shorts             -0.240910
Total Reportable Longs     0.021115
Total Reportable Shorts    0.032472
WTI $/bbl                  1.000000
dtype: float64

In [39]:
Gold_Merge.corrwith(Gold_Merge['Gold $/troy oz'])


Out[39]:
Open Interest              0.737619
Commercial Longs           0.442783
Commercial Shorts          0.371889
Speculator Longs           0.307123
Speculator Shorts          0.296326
Money Manager Longs        0.361752
Money Manager Shorts      -0.002819
Public Longs               0.420261
Public Shorts              0.466503
Total Reportable Longs     0.737520
Total Reportable Shorts    0.735808
Gold $/troy oz             1.000000
dtype: float64

A few interesting observations

  • Correlations typically seem much higher within gold than oil
  • Open Interest and Total Reportable position correlations are similar as Total positions make up Open Interest
  • Money manager shorts have a strong negative correlation with WTI, while open interest has a strong positive correlation with gold

Now, we want to compare the correlations over different time periods

This will allow us to see if the COT reports have predictive power or not


In [40]:
print('WTI Shape:', WTI_Merge.shape)
print('Gold Shape:', Gold_Merge.shape)


WTI Shape: (556, 12)
Gold Shape: (556, 12)
Because the COT reports are weekly, we'll choose a few different time periods to examine over -
  • 1 month (~4 weeks)
  • 2 months (~9 weeks)
  • 3 months (~13 weeks)
  • 6 months (~27 weeks)
  • 12 months (52 weeks) ##### We're examining over different time frames to see if past COT data correlates with future commodity price data, or vice versa

In [41]:
week_list = [9,13,27,52] # Our time periods, less 4 weeks
correl_df1 = pd.DataFrame(WTI_Merge[4:555].corrwith(WTI_Merge['WTI $/bbl'][0:551]), columns = [4])
correl_df2 = pd.DataFrame(Gold_Merge[4:555].corrwith(Gold_Merge['Gold $/troy oz'][0:551]), columns = [4])
# Create the dataframes first with the four week correlations to capture row labels 

for i in week_list:
    correl_df1[i] = list(WTI_Merge[i:555].corrwith(WTI_Merge['WTI $/bbl'][0:555-i]))
    correl_df2[i] = list(Gold_Merge[i:555].corrwith(Gold_Merge['Gold $/troy oz'][0:555-i]))

In [42]:
# Because we don't need the correlation of gold or oil with itself
correl_df1 = correl_df1.drop('WTI $/bbl', axis=0)
correl_df2 = correl_df2.drop('Gold $/troy oz', axis=0)

In [43]:
# Let's use a plotly heatmap on these two datasets to quickly find strong correlations
col_labels = ['1 month', '2 months', '3 months', '6 months', '12 months']
correl_variable = list(correl_df1.index) # For titling 

df1_trace = [go.Heatmap(z = correl_df1.values.tolist(),
                        y = correl_variable,
                        x = col_labels,
                        colorscale = 'Viridis'
                        )]
df2_trace = [go.Heatmap(z = correl_df2.values.tolist(),
                        y = correl_variable,
                        x = col_labels,
                        colorscale = 'Viridis'
                        )]

layout3 = dict(title = 'WTI Correlation Heatmap',
               width= 800,
               height= 600,
               yaxis = dict(tickfont = {'size':6}),
               xaxis = {'title':'COT Correlation Lag Timing'})

layout4 = dict(title = 'Gold Correlation Heatmap',
               width= 800,
               height= 600,
               yaxis = dict(tickfont = {'size':6}),
               xaxis = {'title':'COT Correlation Lag Timing'})

fig = go.Figure(data=df1_trace, layout=layout3)
iplot(fig)

fig = go.Figure(data=df2_trace, layout=layout4)
iplot(fig)


Observations -

  • Gold typically has stronger positive correlations across the board
  • But, looking at gold's price history since 2006, it has appreciated for much of the time period and has been less volatile than WTI
  • Money manager shorts have a clear negative correlation with WTI - this makes sense because as money managers increase their shorts in oil, they are betting more heavily the market will go down. This correlation peaks at the 3 month lag interval
  • It is important to note that only in a few series, mainly money manager shorts on oil and speculator shorts on oil, do correlations became stronger as the lag interval increases - suggesting that only these series could carry some predictive power
  • Finally, while correlations are interesting to observe, they come with many caveats. To truly test whether these reports have any predictive power, we will have to put the strategy to the test and see if it's profitable.

For the remainder of the report I will focus on the WTI money manager shorts category and where it's profitable


In [44]:
# Tracking the correlations over the different time lags
bar_trace = [go.Bar(y = list(correl_df1.loc['Money Manager Shorts']),
                    x = col_labels,
                    opacity = 0.6,
                    marker=dict(
                                color=['rgba(204,204,204,1)', 'rgba(204,204,204,1)',
                                       'rgba(222,45,38,0.8)', 'rgba(204,204,204,1)',
                                       'rgba(204,204,204,1)'])
                   )]
layout5 = dict(title = 'Money Manager Shorts Correlations',
               yaxis = dict(
                            title = 'Money Manager Shorts Correlations',
                            range = [-0.55,-0.65]),
               )

fig = go.Figure(data=bar_trace, layout=layout5)
iplot(fig)



In [45]:
# Also, let's use some of the code above to chart the price of WTI versus money manager shorts
WTI_MM_Trace = go.Scatter( 
            x = WTI_Merge.index,
            y = WTI_Merge['WTI $/bbl'],
            name = 'WTI $/bbl')
WTI_MM_Trace2 = go.Scatter(
            x = WTI_Merge.index,
            y = WTI_Merge['Money Manager Shorts'],
            name = 'MM Shorts',
            yaxis = 'y2')

data3 = [WTI_MM_Trace, WTI_MM_Trace2]

layout6 = dict(
    title = 'WTI Price versus Money Manager Shorts',
    width = 800,
    height = 400,
    yaxis = {'title':'Price ($/bbl)'},
    yaxis2 = dict(
        title='Total Shorts',
        overlaying='y',
        side='right'),
    showlegend = False,
    shapes= [ {
            'type': 'line',
            'x0': '2014-08-10',
            'y0': 0,
            'x1': '2014-08-10',
            'y1': 150,
            'line': {
                'color': 'red',
                'width': 1
            }},
             {
            'type': 'line',
            'x0': '2006-12-23',
            'y0': 0,
            'x1': '2006-12-23',
            'y1': 150,
            'line': {
                'color': 'red',
                'width': 1
            }},
             {
            'type': 'line',
            'x0': '2010-05-20',
            'y0': 0,
            'x1': '2010-05-20',
            'y1': 150,
            'line': {
                'color': 'red',
                'width': 1
            }}]
        )


fig = go.Figure(data=data3, layout=layout6)
iplot(fig)


The chart above provides possible buy or sell signals determined by high short volume and represented by vertical lines

Logically, a buy or sell signal should come when money manager short interest is either very high or low

  • We'll set the boundaries for 'very high' or 'very low' at above the 80th percentile and below the 20th percentile
  • From this, we can determine when interest is above the 80th percentile of all recorded data, a sell signal occurs
  • Similarly, when short interest is below the 20th percentile of all recorded data, a buy signal occurs

In [46]:
# First, set our sell and buy boundaries
Sell_Boundary = WTI_Merge['Money Manager Shorts'].quantile(q = 0.80)
Buy_Boundary = WTI_Merge['Money Manager Shorts'].quantile(q = 0.20)
print('Sell Boundary when short interest is greater than', Sell_Boundary)
print('Buy Boundary when short interest is less than', Buy_Boundary)
# Next, let's look at some descriptive statistics
WTI_Merge['Money Manager Shorts'].describe()


Sell Boundary when short interest is greater than 90079.0
Buy Boundary when short interest is less than 39313.0
Out[46]:
count       556.000000
mean      69129.129496
std       36256.720052
min       12768.000000
25%       46358.500000
50%       62736.000000
75%       83791.750000
max      220139.000000
Name: Money Manager Shorts, dtype: float64

In [47]:
# Now, we'll make two additional columns in our DataFrame for when a buy or sell signal occurs

counter = 0
WTI_Merge['Sell Signal'] = np.nan
WTI_Merge['Buy Signal'] = np.nan

for i in WTI_Merge['Money Manager Shorts']:
    if i >= Sell_Boundary:
        WTI_Merge['Sell Signal'][counter] = 1
        WTI_Merge['Buy Signal'][counter] = 0
    elif i <= Buy_Boundary:
        WTI_Merge['Buy Signal'][counter] = 1
        WTI_Merge['Sell Signal'][counter] = 0
    else:
        WTI_Merge['Sell Signal'][counter] = 0
        WTI_Merge['Buy Signal'][counter] = 0
    counter += 1

In [48]:
# We now have a list of buy and sell signals with 1 denoting True and 0 denoting False

Total_Sells = sum(WTI_Merge['Sell Signal'])
Total_Buys = sum(WTI_Merge['Buy Signal'])
print('The total number of sell orders are', Total_Sells)
print('The total number of buy orders are', Total_Buys)

# Let's also see the average sell signal price and average buy signal price

counter = 0 
sell_sum = 0
buy_sum = 0

for i in WTI_Merge['WTI $/bbl']:
    sell_sum += i * WTI_Merge['Sell Signal'][counter]
    buy_sum += i * WTI_Merge['Buy Signal'][counter]
    counter += 1
# No if statement is needed here because a non-signal is denoted by 0 and will zero out the buy or sell price

print('The average sell price is', sell_sum/Total_Sells)
print('The average buy price is', buy_sum/Total_Buys)


The total number of sell orders are 112.0
The total number of buy orders are 112.0
The average sell price is 58.8345535714
The average buy price is 100.974910714

The more money managers are short (expecting price decreases), the more often the price actually increases. Because of this, we'll reverse our trading signal

  • A buy signal will now be when short interest is greater than the 80th percentile
  • A sell signal will now be when short interest is below the 20th percentile
  • In essence we are trading against the market here, assuming that money managers are so incompetent we want to do exactly the opposite of what they predict

In [49]:
new_col_list = list(WTI_Merge.columns)
new_col_list[-1] = 'New Sell Signal'
new_col_list[-2] = 'New Buy Signal'
WTI_Merge.columns = new_col_list

In [50]:
# Our portfolio will either be completely long or completely short depending on the buy or sell signal
# This is easier said than done. We need to combine the buy and sell signals into one column
# Buys will be signaled by +1, sells by -1, no more 0's

WTI_Merge['Signal Column'] = WTI_Merge['New Buy Signal'] - WTI_Merge['New Sell Signal']
counter = len(WTI_Merge.index) - 1

for i in reversed(WTI_Merge['Signal Column']): # We must loop in reverse because the portfolio will build in an ascending manner
    if i == 0:
        WTI_Merge['Signal Column'][counter] = WTI_Merge['Signal Column'][counter+1] # Signal becomes the one below it
    elif i == 1.0:
        WTI_Merge['Signal Column'][counter] = 1.0
    elif i == -1.0:
        WTI_Merge['Signal Column'][counter] = -1.0
    counter -= 1

In [51]:
# Create a simulated portfolio starting with $10,000 at the beginning of our data
# Portfolio is either 100% long or short as denoted by the single 'Signal Column'

WTI_Merge['Portfolio'] = np.nan
counter = len(WTI_Merge.index)-1
WTI_Merge['Portfolio'][555] = 10000 # Starting portfolio amount 

for i in reversed(WTI_Merge['WTI $/bbl']):
    if WTI_Merge['Signal Column'][counter] == 1.0: # If the previous signal was a buy, you are long the returns on WTI
        WTI_Merge['Portfolio'][counter-1] = ((WTI_Merge['WTI $/bbl'][counter-1] - i)/(i) + 1) * WTI_Merge['Portfolio'][counter]
    elif WTI_Merge['Signal Column'][counter] == -1.0: # If the previous signal was a sell, you are short returns
        WTI_Merge['Portfolio'][counter-1] = (1 - (WTI_Merge['WTI $/bbl'][counter-1] - i)/(i)) * WTI_Merge['Portfolio'][counter]
    counter -= 1 # Drop the counter by one each loop because we are iterating in reverse
    
WTI_Merge['Portfolio'][555] = 10000 
WTI_Merge['Portfolio'].head()


Out[51]:
Date
2017-03-28    20819.858713
2017-03-21    20242.964365
2017-03-14    20337.678362
2017-03-07    22679.697208
2017-02-28    23247.981193
Name: Portfolio, dtype: float64

In [52]:
WTI_Merge.shape


Out[52]:
(556, 16)

Let's take a look at the results - our 'Portfolio' column represents our returns


In [53]:
# Compare to the S&P 500 market portfolio

SP500 = quandl.get("YAHOO/INDEX_GSPC",start_date="2006-06-13",end_date="2017-03-28", collapse='weekly')
SP500['Returns'] = np.nan # Create a column of just percentage returns
counter = 0
SP500['Returns'][0] = 1

for i in SP500['Close']:
    try: # To ignore the necessary 'index out of bounds' error that occurs from [counter+1]
        SP500['Returns'][counter+1] = (((SP500['Close'][counter+1]-i)/i)+1) * SP500['Returns'][counter]  
        counter += 1
    except:
        ''

In [54]:
WTI_Merge['Returns'] = np.nan # Create a column of just percentage returns for the Strategic Portfolio
counter = len(WTI_Merge.index)-1
WTI_Merge['Returns'][counter] = 1

for i in reversed(WTI_Merge['Portfolio']):
    try: # To ignore the necessary 'index out of bounds' error that occurs from [counter+1]
        WTI_Merge['Returns'][counter-1] = (((WTI_Merge['Portfolio'][counter-1]-i)/i)+1) * WTI_Merge['Returns'][counter]  
        counter -= 1
    except:
        ''

In [55]:
# Plot the portfolio return versus the market return 
WTI_Portfolio_Trace = go.Scatter( 
            x = WTI_Merge.index,
            y = WTI_Merge['Returns'],
            name = 'Strategic Portfolio Returns')
SP500_Trace2 = go.Scatter(
            x = SP500.index,
            y = SP500['Returns'],
            name = 'Market Portfolio Returns',
            )

data4 = [WTI_Portfolio_Trace, SP500_Trace2]

layout7 = dict(
    title = 'Strategic versus Market Portfolio',
    width = 1000,
    height = 400,
    yaxis = {'title':'Portfolio Return Multiple'},
    showlegend = True)
    
fig = go.Figure(data=data4, layout=layout7)
iplot(fig)



In [56]:
# Let's take some comparative statistics of these two datasets now
WTI_Merge['Returns'].describe()


Out[56]:
count    556.000000
mean       1.280425
std        0.455300
min        0.510514
25%        0.986091
50%        1.092437
75%        1.486180
max        2.639076
Name: Returns, dtype: float64

In [57]:
SP500['Returns'].describe()


Out[57]:
count    564.000000
mean       1.219213
std        0.320376
min        0.546031
25%        1.002459
50%        1.133096
75%        1.535247
max        1.904150
Name: Returns, dtype: float64

In [58]:
# Finally, just comparing some last metrics about usefulness 

Sharpe = (WTI_Merge['Returns'][0]-SP500['Returns'][0])/WTI_Merge['Returns'].describe()['std']
Max_Drawdown = WTI_Merge['Returns'].describe()['min']

print('The strategy Sharpe ratio is', Sharpe)
print('The strategy max drawdown is', Max_Drawdown)


The strategy Sharpe ratio is 2.37642540978
The strategy max drawdown is 0.510514018692

Conclusion

With the caveats of "correlation does not equal causation" and "past results do not predict future returns" in mind, this trading strategy still produced some impressive returns compared to the market portfolio. The Sharpe Ratio measures excess returns from the market per unit of additional risk taken on. In this case, a Sharpe of 2.38 shows that the trading strategy provides an impressive level of return per unit of risk taken on compared to the S&P. Despite this extensive analysis, there are still more ways this strategy would need to be tested before being practical. A few factors that were left out...

  • Transactions costs, taxes, liquidity, spread costs, etc. would all decrease from expected profitability
  • A longer time frame would be helpful in examining how this strategy works in more than just one market contraction period
  • Examining certain more profitable periods of the strategy to determine what market conditions are most favorable

It appears that the strategy was most productive in times where oil prices were steadily increasing (2007 to mid-2008) or steadily falling (2014-2015). By adding some sort of market volatility input, it's possible we could have developed an on/off switch for when the strategy would be active.

Overall, this is an interesting look into the COT reports which suggests that they may not have any broad predictive powers but they certainly can carry useful information.